#include "src/scene/spherePrimitive.h"

#include <GL/gl.h>
#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>

#include "src/defines/primitive.h"
#include "src/defines/axes.h"
#include "src/defines/rayState.h"
#include "src/defines/functions.h"
#include "src/math/vector.h"

#define START_PHI 80
#define PHI 16
#define THETA 18
#define DRAW_NORMAL 0

SpherePrimitive::SpherePrimitive(int _id) : Object(_id){
    setBoundingBox();
}

SpherePrimitive::SpherePrimitive(const SpherePrimitive &_s, int _id)
    : Object(_s.material, _s.getCenter(), _id, SPHERE, true, _s.transformation), radius(_s.radius){
        setBoundingBox();
}

SpherePrimitive::SpherePrimitive(const Material &_m, const Vector3D &_center, double _radius, int _id)
    : Object(_m, _center, _id, SPHERE, true), radius(_radius){
//    transformation.addScale(Vector3D(radius, radius, radius));
        setBoundingBox();
}

SpherePrimitive::SpherePrimitive(const Vector3D &_center, double _radius, const Material &_m, const Matrix &_matrix, const Matrix &_matrixI, int _id)
    : Object(_m, _center, _id, SPHERE, true, _matrix, _matrixI), radius(_radius){
        setBoundingBox();
}

SpherePrimitive::~SpherePrimitive(){
}

double SpherePrimitive::getRadius() const{
    return radius;
}

void SpherePrimitive::setRadius(double _r){
    radius = _r;
//    transformation.addScale(Vector3D(radius, radius, radius));
}

double SpherePrimitive::volume() const{
    return (M_PI*radius*radius*radius*4.0)/3.0;
}

double SpherePrimitive::area() const{
    return (M_PI*radius*radius*4.0);
}

void SpherePrimitive::draw(bool _wireframe, bool _useParentColor, const Material &_parentColor) const{

    double minTexU, maxTexU, minTexV, maxTexV;
    bool useTexture = drawBegin(_wireframe, _useParentColor, _parentColor, &minTexU, &maxTexU, &minTexV, &maxTexV);



    if(useTexture)
    {
        //sem triangle fun
        glBegin(GL_QUAD_STRIP);
    //    glColor4d(color_.ambient.red, color_.ambient.green, color_.ambient.blue, color_.ambient.alpha);
        for(double i=88.0; i > -88.0; i = i-PHI){
            for(double j=0; j<=360; j=j+THETA){

                double theta = Functions::toRad(j);
                double phi = Functions::toRad(i);
                double sinPhi = sin(phi);
                double x = radius*cos(theta)*cos(phi);
                double y = radius*sin(theta)*cos(phi);
                double z = radius*sinPhi;

                Vector3D normal = (Vector3D(x, y, z)).normalize();
                glNormal3f(normal.x, normal.y, normal.z);
                glTexCoord2d((j*maxTexU)/360.0, ((sinPhi+1)/2.0)*maxTexV);
                glVertex3d(x, y, z);

                phi = Functions::toRad(i - PHI);
                sinPhi = sin(phi);
                x = radius*cos(theta)*cos(phi);
                y = radius*sin(theta)*cos(phi);
                z = radius*sinPhi;

                normal = (Vector3D(x, y, z)).normalize();
                glNormal3f(normal.x, normal.y, normal.z);
                glTexCoord2d((j*maxTexU)/360.0, ((sinPhi+1)/2.0)*maxTexV);
                glVertex3d(x, y, z);
            }
        }
        glEnd();
    }
    else
    {

        glBegin(GL_TRIANGLE_FAN);
        Vector3D normal = (Vector3D(0.0, 0.0, radius)).normalize();
        glNormal3f(normal.x, normal.y, normal.z);
        glVertex3d(0.0, 0.0, radius);

        double startCos = cos(Functions::toRad(START_PHI));
        double startSin = sin(Functions::toRad(START_PHI));
        for(double i=0; i<=360.0; i = i+THETA){
            double theta = Functions::toRad(i);
            double x = radius*cos(theta)*startCos;
            double y = radius*sin(theta)*startCos;
            double z = radius*startSin;

            normal = (Vector3D(x, y, z)).normalize();
            glNormal3f(normal.x, normal.y, normal.z);

            glVertex3d(x, y, z);
        }
        glEnd();

    //    #if DRAW_NORMAL
    //    {
    //        glBegin(GL_LINES);
    //        normal = (Vector3D(0.0, 0.0, radius)).normalize();
    //        glVertex3d(0.0, 0.0, radius);
    //        glVertex3d(0.0 + normal.x, 0.0 + normal.y, radius + normal.z);
    //
    //        for(double i=0; i<=360.0; i = i+THETA){
    //            double theta = Functions::toRad(i);
    //            double x = radius*cos(theta)*cos(Functions::toRad(START_PHI));
    //            double y = radius*sin(theta)*cos(Functions::toRad(START_PHI));
    //            double z = radius*sin(Functions::toRad(START_PHI));
    //
    //            normal = (Vector3D(x, y, z)).normalize();
    //            glVertex3d(x, y, z);
    //            glVertex3d(x + normal.x, y + normal.y, z + normal.z);
    //        }
    //        glEnd();
    //    }
    //    #endif //DRAW_NORMAL


        glBegin(GL_QUAD_STRIP);
    //    glColor4d(color_.ambient.red, color_.ambient.green, color_.ambient.blue, color_.ambient.alpha);
        for(double i=START_PHI; i > -START_PHI; i = i-PHI){
            for(double j=0; j<=360; j=j+THETA){

                double theta = Functions::toRad(j);
                double phi = Functions::toRad(i);
                double sinPhi = sin(phi);
                double x = radius*cos(theta)*cos(phi);
                double y = radius*sin(theta)*cos(phi);
                double z = radius*sinPhi;

                normal = (Vector3D(x, y, z)).normalize();
                glNormal3f(normal.x, normal.y, normal.z);
                glVertex3d(x, y, z);

                phi = Functions::toRad(i - PHI);
                sinPhi = sin(phi);
                x = radius*cos(theta)*cos(phi);
                y = radius*sin(theta)*cos(phi);
                z = radius*sinPhi;

                normal = (Vector3D(x, y, z)).normalize();
                glNormal3f(normal.x, normal.y, normal.z);
                glVertex3d(x, y, z);
            }
        }
        glEnd();


    //    #if DRAW_NORMAL
    //    {
    //        glBegin(GL_LINES);
    //        for(double i=START_PHI; i > -START_PHI; i = i-PHI){
    //            for(double j=0; j<=360; j=j+THETA){
    //
    //                double theta = Functions::toRad(j);
    //                double phi = Functions::toRad(i);
    //                double x = radius*cos(theta)*cos(phi);
    //                double y = radius*sin(theta)*cos(phi);
    //                double z = radius*sin(phi);
    //
    //                normal = (Vector3D(x, y, z)).normalize();
    //                glVertex3d(x, y, z);
    //                glVertex3d(x + normal.x, y + normal.y, z + normal.z);
    //
    //                phi = Functions::toRad(i - PHI);
    //                x = radius*cos(theta)*cos(phi);
    //                y = radius*sin(theta)*cos(phi);
    //                z = radius*sin(phi);
    //
    //                normal = (Vector3D(x, y, z)).normalize();
    //                glVertex3d(x, y, z);
    //                glVertex3d(x + normal.x, y + normal.y, z + normal.z);
    //            }
    //        }
    //        glEnd();
    //    }
    //    #endif //DRAW_NORMAL

        glBegin(GL_TRIANGLE_FAN);
    //    glColor4d(color_.ambient.red, color_.ambient.green, color_.ambient.blue, color_.ambient.alpha);
        normal = (Vector3D(0.0, 0.0, -radius)).normalize();
        glNormal3f(normal.x, normal.y, normal.z);
        glVertex3d(0.0, 0.0, -radius);

        double endCos = cos(Functions::toRad(-START_PHI));
        double endSin = sin(Functions::toRad(-START_PHI));
        for(double i=0; i<=360.0; i = i+THETA){
            double theta = Functions::toRad(-i);
            double x = radius*cos(theta)*endCos;
            double y = radius*sin(theta)*endCos;
            double z = radius*endSin;

            Vector3D normal = (Vector3D(x, y, z)).normalize();
            glNormal3f(normal.x, normal.y, normal.z);

            glVertex3d(x, y, z);
        }
        glEnd();
    //
    //    #if DRAW_NORMAL
    //    {
    //        glBegin(GL_LINES);
    //        normal = (Vector3D(0.0, 0.0, -radius)).normalize();
    //        glVertex3d(0.0, 0.0, -radius);
    //        glVertex3d(0.0 + normal.x, 0.0 + normal.y, radius + normal.z);
    //
    //        for(double i=0; i<=360.0; i = i+THETA){
    //            double theta = Functions::toRad(i);
    //            double x = radius*cos(theta)*cos(Functions::toRad(START_PHI));
    //            double y = radius*sin(theta)*cos(Functions::toRad(START_PHI));
    //            double z = radius*sin(Functions::toRad(START_PHI));
    //
    //            normal = (Vector3D(x, y, z)).normalize();
    //            glVertex3d(x, y, z);
    //            glVertex3d(x + normal.x, y + normal.y, z + normal.z);
    //        }
    //        glEnd();
    //    }
    //    #endif //DRAW_NORMAL
    }

	drawEnd(_wireframe, useTexture);
}

std::vector<Trio> SpherePrimitive::intersection(const Ray &_ray) const{
    std::vector<Trio> result;

    Vector3D origin = _ray.getOrigin();

//    #if DEBUG
//    bool test = false;
//    if(origin != Vector3D(0, -5, 5)){ test = true; std::cout <<"Raio de origem "; origin.print();}
//    #endif //DEBUG

    //inserindo o primeiro ponto que TODOS terão
    result.push_back(Trio(origin, Vector3D(1,0,0), OUT));

    //colocnado o raio para coordenadas da primitiva
    Matrix toLocalVector = transformation.getWorldToLocal();
    Matrix toLocalDirection = transformation.getWorldToLocalDirection();

    origin = (toLocalVector * origin.toHomogeneousCoordinates()).fromHomogeneousCoordinates();
    Vector3D direction = Vector3D(toLocalDirection * Vector(_ray.getDirection()));



//    std::cout << "Tem os vetores origem e direção nas coordenadas locais"<<std::endl;
//    std::cout << "Origem" <<std::endl;
//    origin.print();
//    std::cout << "Direção" <<std::endl;
//    direction.print();
//
//    std::cout << "Centro" <<std::endl;
//    center.print();
//    Vector3D originCenter = origin - center;

    double a = direction.dotProduct(direction);
    double b = direction.dotProduct(origin) * 2.0;
    double c = origin.dotProduct(origin) - (radius*radius);
    //*** aqui poderia não precisar o raio ao quadrado se tivesse a trasnformação

    double delta = (b*b) - (4.0 * a * c);


//    #if DEBUG
//    if(test){ std::cout <<"Delta " << delta << std::endl;}
//    #endif //DEBUG

//    std::cout << "\nValores\n\tA = " <<a <<"\n\tB = " << b << "\n\tC = " << c << "\n\tDelta = " <<delta<<std::endl;
//    exit(1);


    //se for menor que zero
    //não existe interseção
    //não insere nada
    if(fabs(delta) < ERROR){ //delta igual a zero
        double t = - b / (2.0 * a);

        Matrix toWorld = transformation.getLocalToWorld();
        Matrix toLocalTranpost = transformation.getWorldToLocalDirection().transpose();

        Vector3D localP = origin + (direction * t);
        Vector3D p = (toWorld * (localP).toHomogeneousCoordinates()).fromHomogeneousCoordinates();
        Vector3D n = Vector3D(toLocalTranpost * Vector(normal(localP))).normalize();

        if(fabs(t) < ERROR){ //se t == 0
            result.back().type = ON;
            result.back().normal = n;
            result.push_back(Trio(p, n, OUT));
        }else{
            result.push_back(Trio(p, n, ON));
            result.push_back(Trio(p, n, OUT));
        }

    }else if(delta > 0.0){
        double t0 = (- b - sqrt(delta)) / (2.0 * a);
        double t1 = (- b + sqrt(delta)) / (2.0 * a);

        if(t0 <= 0.0){ //t0 é menor ou igual zero, então já começa dentro
            Matrix toWorld = transformation.getLocalToWorld();
            Matrix toLocalTranpost = transformation.getWorldToLocalDirection().transpose();

            Vector3D localP0 = origin + (direction * t0);
            Vector3D localP1 = origin + (direction * t1);
            Vector3D n0 = Vector3D(toLocalTranpost * Vector(normal(localP0))).normalize();
            Vector3D n1 = Vector3D(toLocalTranpost * Vector(normal(localP1))).normalize();

            if(fabs(t0) < ERROR){
                result.back().type = ON;
                result.back().normal = n0;
                result.push_back(Trio(result.back().point, n0, OUT));
            }
            if(t1 > 0.0){//se t1 maior que zero, então ele está pra frente
                result.back().type = IN;

                Vector3D p1 = (toWorld * (localP1).toHomogeneousCoordinates()).fromHomogeneousCoordinates();
                result.push_back(Trio(p1, n1, OUT));
            }else if(fabs(t1) < ERROR){
                result.back().type = ON;
                result.back().normal = n1;
                result.push_back(Trio(result.back().point, n1, OUT));
            }
        }else{
            //os dois valores são positivos
            Matrix toWorld = transformation.getLocalToWorld();
            Matrix toLocalTranpost = transformation.getWorldToLocalDirection().transpose();

            Vector3D localP0 = origin + (direction * t0);
            Vector3D localP1 = origin + (direction * t1);
            Vector3D n0 = Vector3D(toLocalTranpost * Vector(normal(localP0))).normalize();
            Vector3D n1 = Vector3D(toLocalTranpost * Vector(normal(localP1))).normalize();

            Vector3D p0 = (toWorld * (localP0).toHomogeneousCoordinates()).fromHomogeneousCoordinates();
            result.push_back(Trio(p0, n0, IN));

            Vector3D p1 = (toWorld * (localP1).toHomogeneousCoordinates()).fromHomogeneousCoordinates();
            result.push_back(Trio(p1, n1, OUT));

        }
    }
    return result;
}

void SpherePrimitive::setBoundingBox() {
    boundingBoxWithTransformation(Vector3D(-radius, -radius, -radius), Vector3D(radius, radius, radius));
}

Vector3D SpherePrimitive::normal(const Vector3D &_p) const{
    return (_p/* - center*/).normalize();
}

void SpherePrimitive::getTextureUV(double *_u, double *_v, const Vector3D &_point, unsigned int _type) const
{
    Matrix toLocal = transformation.getWorldToLocal();
    Vector3D localPoint = (toLocal * (_point).toHomogeneousCoordinates()).fromHomogeneousCoordinates();

    if( fabs(localPoint.z - 1) < ERROR ) localPoint.z = 1;
    else if( fabs(localPoint.z + 1) < ERROR ) localPoint.z = -1;
    if( fabs(localPoint.y - 1) < ERROR ) localPoint.y = 1;
    else if( fabs(localPoint.y + 1) < ERROR ) localPoint.y = -1;
    if( fabs(localPoint.x - 1) < ERROR ) localPoint.x = 1;
    else if( fabs(localPoint.x + 1) < ERROR ) localPoint.x = -1;

    if(_type == PLANAR_TEXTURE)
    {
        *_u = 0.5 + localPoint.x/(2.0);
        *_v = 0.5 + localPoint.z/(2.0);//sempre pensando que o raio é 1;
    }
    else
    {
        *_u = 0.5 - (atan2(localPoint.y, -localPoint.x)/(2*M_PI));
        *_v = 0.5 - (asin(localPoint.z)/(M_PI));
    }
}
